﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media.Media3D;
using System.Windows.Input;
using System.Windows;

namespace CameraControllers
{
	public class OrbitalCameraController
	{
		// internal constantes
		private const double MinimumDirectionLength = 0.001;
		private const double MoveRatio = 0.0025;
		private const double RotationRatio = 0.01;
		private const double ZoomRatio = 0.005;
		private const double ZoomMinimum = 0.9;
		private const double ZoomMaximum = 1.2;
		private const double RotationEpsilon = 0.001;

		private PerspectiveCamera m_Camera = new PerspectiveCamera();
		private Vector3D m_Position;
		private Vector3D m_Up;
		private Vector3D m_Target;
		private Vector3D m_LookDir;
		private double m_LookDirLen;
		private Vector3D m_LookRight;
		private Vector3D m_LookUp;
		private Point m_MousePrevPosition;

		/// <summary>
		/// Creates a default orbital camera controller, setting the camera on the position (0;0;1), targeting (0;0;0).
		/// </summary>
		public OrbitalCameraController()
			: this(new Vector3D(0.0, 0.0, 10.0), new Vector3D())
		{
		}

		/// <summary>
		/// Creates an orbital camera controller, targeting (0;0;0).
		/// </summary>
		/// <param name="position">Start position of the camera.</param>
		public OrbitalCameraController(Vector3D position)
			: this(position, new Vector3D())
		{
		}

		/// <summary>
		/// Creates an orbital camera controller.
		/// </summary>
		/// <param name="position">Position of the camera.</param>
		/// <param name="target">Location the camera targets.</param>
		public OrbitalCameraController(Vector3D position, Vector3D target)
		{
			m_Position = position;
			m_Target = target;
			m_Up = new Vector3D(0.0, 1.0, 0.0);

			UpdateParams();
		}

		/// <summary>
		/// Gets the subjacent Camera instance.
		/// </summary>
		public Camera Camera
		{
			get
			{
				return (m_Camera);
			}
		}

		/// <summary>
		/// Gets or sets the position of the camera.
		/// </summary>
		public Vector3D Position
		{
			get
			{
				return (m_Position);
			}
			set
			{
				m_Position = value;
				UpdateParams();
			}
		}

		/// <summary>
		/// Gets or sets the location the camera targets.
		/// </summary>
		public Vector3D Target
		{
			get
			{
				return (m_Target);
			}
			set
			{
				m_Target = value;
				UpdateParams();
			}
		}

		/// <summary>
		/// Gets or sets the up vector of the camera.
		/// </summary>
		public Vector3D Up
		{
			get
			{
				return (m_Up);
			}
			set
			{
				m_Up = value;
				UpdateParams();
			}
		}

		/// <summary>
		/// Gets the view vector (normalized) of the camera.
		/// </summary>
		public Vector3D ViewVector
		{
			get
			{
				return (m_LookDir);
			}
		}

		/// <summary>
		/// Gets the right direction vector (normalized) of the camera.
		/// </summary>
		public Vector3D RightVector
		{
			get
			{
				return (m_LookRight);
			}
		}

		/// <summary>
		/// Gets the up vector (normalized) of the camera.
		/// </summary>
		public Vector3D UpVector
		{
			get
			{
				return (m_LookUp);
			}
		}

		/// <summary>
		/// Moves the camera in the given direction.
		/// </summary>
		/// <param name="direction">Direction vector to move the camera. Intensity of the vector is taken into account.</param>
		public void Move(Vector3D direction)
		{
			m_Position += direction;
			m_Target += direction;

			UpdateParams();
		}

		/// <summary>
		/// Rotates the camera around the target point.
		/// </summary>
		/// <param name="ry">Rotation angle around the Y axis, in radians.</param>
		/// <param name="rx">Rotation angle around the X axis, in radians.</param>
		public void Rotate(double ry, double rx)
		{
			double f = Math.Acos(Vector3D.DotProduct(m_LookDir, m_Up));
			if (rx > 0.0f)
				f = Math.Min(rx, f - RotationEpsilon);
			else
				f = Math.Max(rx, f - (float)Math.PI + RotationEpsilon);

			Transform3DGroup trGroup = new Transform3DGroup();
			trGroup.Children.Add(new RotateTransform3D(new AxisAngleRotation3D(m_Up, ry * 180.0 / Math.PI)));
			trGroup.Children.Add(new RotateTransform3D(new AxisAngleRotation3D(m_LookRight, f * 180.0 / Math.PI)));
			Matrix3D final = trGroup.Value;

			m_LookDir = m_LookDir * final;
			m_Position = m_Target - m_LookDir * m_LookDirLen;

			UpdateParams();
		}

		/// <summary>
		/// Zooms in or out.
		/// </summary>
		/// <remarks>This moves the camera forward/backward along the view vector, so camera position is modified when performing a zoom.</remarks>
		/// <param name="zoom">Moving coeficient relative to the distance between camera position and target (unitless).</param>
		public void Zoom(double zoom)
		{
			double len = m_LookDirLen * zoom;
			len = Math.Max(MinimumDirectionLength, len);
			m_LookDir.Normalize();
			m_Position = m_Target - m_LookDir * len;

			UpdateParams();
		}

		/// <summary>
		/// Recomputes the view, right and up vectors.
		/// </summary>
		private void UpdateParams()
		{
			m_LookDir = m_Target - m_Position;
			m_LookDirLen = m_LookDir.Length;

			m_LookRight = Vector3D.CrossProduct(m_LookDir, m_Up);
			m_LookRight.Normalize();

			m_LookUp = Vector3D.CrossProduct(m_LookRight, m_LookDir);
			m_LookUp.Normalize();

			m_LookDir.Normalize();

			m_Camera.Position = new Point3D(m_Position.X, m_Position.Y, m_Position.Z);
			m_Camera.LookDirection = m_LookDir;
			m_Camera.UpDirection = m_Up;
		}

		private bool[] m_MouseDown = new bool[3];

		/// <summary>
		/// Updates the camera attributes.
		/// </summary>
		/// <param name="e">The mouse events to update the camera. It can be set to null.</param>
		public void Update(MouseEventArgs e)
		{
			if (e != null)
			{
				//Point pos = e.GetPosition(e.MouseDevice.Target);
				Point pos = MouseCheck(e);

				// compute mouse movement delta
				double mousedx = pos.X - m_MousePrevPosition.X;
				double mousedy = pos.Y - m_MousePrevPosition.Y;

				// rotate camera
				if (e.LeftButton == MouseButtonState.Pressed)
				{
					double dx = RotationRatio * mousedx;
					double dy = RotationRatio * mousedy;
					Rotate(-dx, -dy);
				}

				// translate camera
				if (e.MiddleButton == MouseButtonState.Pressed)
				{
					Vector3D vec = m_LookRight * -mousedx + m_LookUp * mousedy;
					Move(vec * m_LookDirLen * MoveRatio);
				}

				// zoom
				if (e.RightButton == MouseButtonState.Pressed)
				{
					double z = 1.0 + mousedy * ZoomRatio;
					z = Math.Max(ZoomMinimum, Math.Min(z, ZoomMaximum));
					Zoom(z);
				}

				// save the current position to use it again on next frame
				m_MousePrevPosition = pos;
			}
		}

		/// <summary>
		/// Check mouse status to avoid camera misplacement when mouse moves while a model window disappears.
		/// </summary>
		/// <param name="e">Mouse event information.</param>
		/// <returns>Returns the current mouse pointer location.</returns>
		private Point MouseCheck(MouseEventArgs e)
		{
			MouseCheckElement(e, e.LeftButton, 0);
			MouseCheckElement(e, e.MiddleButton, 1);
			MouseCheckElement(e, e.RightButton, 2);

			return (e.GetPosition(e.MouseDevice.Target));
		}

		/// <summary>
		/// Check a specific mouse button and stores the proper button status.
		/// </summary>
		/// <param name="e">Mouse event information.</param>
		/// <param name="btn">Specific button to check.</param>
		/// <param name="index">Index of the button (for internal use).</param>
		private void MouseCheckElement(MouseEventArgs e, MouseButtonState btn, int index)
		{
			if (btn == MouseButtonState.Pressed)
			{
				if (m_MouseDown[index] == false)
				{
					m_MousePrevPosition = e.GetPosition(e.MouseDevice.Target);
					m_MouseDown[index] = true;
				}
			}
			else
			{
				m_MouseDown[index] = false;
			}
		}
	}
}
